在後面幾個比較深的房間裡,我訝異的發現,那些短箭頭,還能夠進化成更加特別的形狀…
在 Monoid 的章節裡,我們看過了 id
這個什麼事都不做的函式。而現在要介紹一個差不多奇怪的函式,叫做 const
。這個函式接收兩個參數,然後原封不動的回傳第一個參數。還有,const
是一個懶惰的函式。
-- Haskell 語法
const :: a -> b -> a
const a b = a -- 已內建,只是秀出來讓你們羨慕一下
-- 使用
toOne = const 1
-- 呼叫
toOne "test" -- => 1
如果用其它語言,如 JavaScript ,就得要自己實作const
了,像是這樣:
// JavaScript 語法
let const = a => b => a //寫出來之後,發現好像也沒有多值得羨慕了
// 試試看
let toOne = const(1)
toOne(100) //=> 1
或 Elixir
# Elixir 語法
const = fn a -> fn b -> a end end
# 試試看
to_one = const.(1)
to_one.(999) #=> 1
這個函式乍見跟 id
一樣沒啥路用。但是它的其中一個功能,就是來做為無視輸入,進行置換用的函式。而我們要用這個函式與 fmap
搭配,做出魔法般的事。
我們一直說要把容器抽象來看這件事,在此終於可以體現其真正的優勢了。我們先來假設一個多層的資料結構:
-- Haskell 語法
things = [
Just "hello world",
Nothing,
Just "Haskell",
Just "Elixir"
]
雖然看似簡單,但是當我們帶上 functor 的視角來看時,這是一個三層的 functor。第一層、也就是最外層是個串列,第二層是裡面一個個的 Maybe String,而第三層就是 Just 裡面的那個 String 了。串列、Maybe 跟 String 都是 functor 的,記得嗎?
接下來,我們再來做一個無視輸入是什麼,都會回傳一個字元 'A'
的函式。
-- Haskell 語法
toA = const 'A'
toA things -- => 'A'
fmap toA things
-- => "AAAA"
(fmap . fmap) toA things
-- => [Just 'A', Nothing, Just 'A', Just 'A']
(fmap . fmap . fmap) toA things
-- => [Just "AAAAAAAAAAA", Nothing, Just "AAAAAAA", Just "AAAAAA"]
再來看一下 fmap 及其組合們的型別:
-- Haskell 語法
fmap :: Functor f =>
(a -> b) -> f a -> f b
(fmap . fmap)
:: (Functor f1, Functor f2) =>
(a -> b) -> f1 (f2 a) -> f1 (f2 b)
(fmap . fmap . fmap)
:: (Functor f1, Functor f2, Functor f3) =>
(a -> b) -> f1 (f2 (f3 a)) -> f1 (f2 (f3 b))
當然,一如往常,我們還是要問最後一個問題:
我們之前一直提到,函式是一種計算過程的容器,就是預備著當我們走到這裡時,可以啟發看到這個本質的洞見。
舉例來說,當我們有一個 \a -> a * 3 + 1
的函式時,我們可以把它看成一個裝在 a ->
的容器中,元素是 a * 3 + 1
的值,只是這個值是一種計算過程。
把這個抽象化來看,不管那個計算有多複雜,當我們有一個 a -> b
的函式時,用容器的角度來看,這是一個裝載在 a ->
容器裡的 b
的值。當然就像上面所說的,這個 b
會是個計算,簡單的或複雜的都沒差。
那當我們用把一個函式視為容器,對它進行 fmap
,例如我們傳了 f
這個函式當做第一個參數。那麼依照 functor 那個保留原來的外殼,而用裡面的值呼叫函式這個行為,那麼會變成這樣:
-- Haskell
fmap f (\a -> b)
-- 會變成
\a -> f(b)
如果你退後一點看,你會發現,這就是把原先計算的結果,再傳遞給…f
這個函式。
是的,用 f
函式去 famp
另一個函式 g
,其行為就跟用 f
與 g
進行函式組合是一模一樣的。
-- Haskell 語法
fmap f g
--- 等同於
f . g
讓我們把 fmap
作用在函式上的型別限制寫出來,雖然意思跟函式組合是相同的,這個型別標註,更能看出來函式是一種容器的特性:
-- Haskell 語法
(<$>) @((->) _) :: (a -> b) -> (_ -> a) -> _ -> b
-- 把最後兩個括起來會更加明顯:
(<$>) @((->) _) :: (a -> b) -> (_ -> a) -> (_ -> b)
-- 跟函式組合(最後的部份加上括號)對比
(.) :: (b -> c) -> (a -> b) -> (a -> c)
我們能夠驗證,函式的 functor 實體,也能夠符合那三條法則嗎?
-- Haskell 語法
f = (+1)
g = (*2)
h = (+3)
-- 封閉律與單位元素
fmp = fmap id f
fid = id f
f 1 -- => 2
fmp 1 -- => 2
fid 1 -- => 2
-- 分配律
hc = fmap (f . g) h
hd = fmap f . fmap g $ h
hc 1 -- => 9
hd 1 -- => 9
既然我們可以對函式 fmap
,而 fmap
本身也是一個…嗯…函式,那我們可不可以對 fmap
進行 fmap
?當然!
fmap f fmap
-- 等同於
f . fmap
fmap fmap fmap -- 嘿!?
-- 等同於
fmap . fmap
fmap fmap $ fmap fmap fmap -- 住、住手!
-- 等同於
fmap . fmap . fmap
我們知道了 <>
用在函式上等同於.
進行函式組合。我們現在又瞭解 <$>
也是函式組合,差別只在它不限制回傳值的型需要是一樣的 Monoid。雖然依然感覺一點用都沒有,相當空虛,不過為了理解接下來的型別,這兩件事其實很重要。
雖然外表上看得出來,但在無數次來回這世界,一間間的房間探索與試錯之後,才深刻的體會到,這一棟建築怎麼會大成這個樣子?就在我以為可能永遠都走不到盡頭的時候,就打開了那擺著平台的房間的門。我深呼吸了幾次,才向著房間裡面走去……
[to be continue]